package com.ccx.demo.config.init;

import com.google.common.base.Strings;
import com.support.mvc.encrypt.AesApi;
import com.support.mvc.encrypt.AesApiId;
import com.support.mvc.entity.convert.AesDbConvert;
import com.utils.aes.IvRandomRule;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.commons.util.InetUtils;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.utils.util.Dates.Pattern.yyyyMMddHHmmss;

/**
 * 初始化 application.properties 中的应用配置参数；
 *
 * @author 谢长春 on 2018-10-2
 */
@Component
@Getter
@Setter
@Slf4j
@ConfigurationProperties("spring.app")
public class AppConfig {
    private static volatile AppConfig INSTANCE = null;

    /**
     * 获取 {@link AppConfig} 实例，用于静态类或实体类获取配置参数
     *
     * @return {@link AppConfig}
     */
    public static AppConfig instance() {
        if (Objects.isNull(INSTANCE)) {
            synchronized (AppConfig.class) {
                if (Objects.isNull(INSTANCE)) {
                    INSTANCE = AppInit.getAppContext().getBean(AppConfig.class);
                }
            }
        }
        return INSTANCE;
    }

    @SneakyThrows
    public AppConfig(InetUtils inetUtils) {
        this.ip = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
        this.startTime = yyyyMMddHHmmss.now();
//        Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
//        Set<String> ipAddressSet = Sets.newLinkedHashSet();
//        while (networkInterfaces.hasMoreElements()) {
//            Enumeration<InetAddress> inetAddress = networkInterfaces.nextElement().getInetAddresses();
//            while (inetAddress.hasMoreElements()) {
//                InetAddress address = inetAddress.nextElement();
//                if (address instanceof Inet4Address) {
//                    if (!"localhost".equals(address.getHostName())) {
//                        ipAddressSet.add(address.getHostAddress());
//                    }
//                }
//            }
//        }
//        this.ip = String.join(",", ipAddressSet);
    }

    /**
     * 当前环境：[local:本地开发环境|dev:远程开发环境|sit:系统测试|uat:验收测试|prod:生产]
     */
    private String env;
    /**
     * 当前主机 IP 地址. 使用 {@link InetUtils} 获取，不需要配置
     */
    private String ip;
    /**
     * 当前服务端口
     */
    @Value("${server.port:8080}")
    private int port;
    /**
     * 应用名
     */
    @Value("${spring.application.name}")
    private String name;
    /**
     * 当前运行版本号
     */
    @Value("${spring.application.version}")
    private String version;
    /**
     * 项目编译打包时间，发布完成之后检查打包时间是否是一致的，避免版本更新失败
     */
    @Value("${spring.application.build-time}")
    private String buildTime;
    /**
     * 项目启动时间
     */
    private String startTime;
    /**
     * 是否开启跨域 true：是，false：否.
     */
    private boolean corsEnabled;
    /**
     * 是否开启验证码返回，开启之后前端调用图片验证码接口时返回真实的验证码，一般用于自动化测试，生产环境请关闭该配置 true：是，false：否.
     */
    private boolean showVerifyCodeEnabled;
    /**
     * 是否开启手机验证码校验，开发环境关闭该开关可以用任意验证码登录，一般用于自动化测试和开发环境，生产环境请启用该配置 true：是，false：否.
     */
    private boolean verifyPhoneCodeEnabled;
    /**
     * 是否开启 swagger 文档 true：是，false：否.
     */
    private boolean swaggerEnabled;
    /**
     * 加密配置
     */
    private Encrypt encrypt;
    /**
     * 应用文件存储
     */
    private AppFiles files;

    /**
     * 判断当前是否属于本地开发环境
     *
     * @return boolean true：本地开发环境
     * @deprecated 废弃方法，不要用环境判断是否启用 X 功能，加一个开关比判断环境更灵活
     */
    @Deprecated
    public boolean isDev() {
        return Objects.equals("dev", env);
    }

    /**
     * 判断当前是否属于云开发测试环境
     *
     * @return boolean true：云开发测试环境
     * @deprecated 废弃方法，不要用环境判断是否启用 X 功能，加一个开关比判断环境更灵活
     */
    @Deprecated
    public boolean isSit() {
        return Objects.equals("sit", env);
    }

    /**
     * 判断当前是否属于线上生产环境
     *
     * @return boolean true：线上生产环境
     * @deprecated 废弃方法，不要用环境判断是否启用 X 功能，加一个开关比判断环境更灵活
     */
    @Deprecated
    public boolean isUat() {
        return Objects.equals("uat", env);
    }

    /**
     * 判断当前是否属于线上生产环境
     *
     * @return boolean true：线上生产环境
     * @deprecated 废弃方法，不要用环境判断是否启用 X 功能，加一个开关比判断环境更灵活
     */
    @Deprecated
    public boolean isProd() {
        return Objects.equals("prod", env);
    }

    /**
     * 加密配置
     */
    @Getter
    @Setter
    @ToString
    public static class Encrypt {
        private PwdEncrypt pwd;
        /**
         * 全局 aes 密钥
         */
        private AesEncrypt api;
        /**
         * 全局 aes 密钥
         */
        private AesEncrypt db;

        /**
         * 接口加解密
         *
         * @param api {@link AesEncrypt}
         */
        public void setApi(AesEncrypt api) {
            this.api = api;
            AesApi.setEnabled(api.isEnabled());
            AesApi.setSecretKey(api.getSecretKey()); // 设置全局 aes 密钥
            if (!Strings.isNullOrEmpty(api.getIvParameterSpec())) {
                AesApi.setIV(api.getIvParameterSpec());
            }
            if (Objects.nonNull(api.getIvRandomRule())) {
                AesApi.setIvRandomRule(api.getIvRandomRule());
            }
            AesApiId.setEnabled(api.isEnabled());
            AesApiId.setSecretKey(api.getSecretKey()); // 设置全局 aes 密钥
            if (!Strings.isNullOrEmpty(api.getIvParameterSpec())) {
                AesApiId.setIV(api.getIvParameterSpec());
            }
            if (Objects.nonNull(api.getIvRandomRule())) {
                AesApiId.setIvRandomRule(api.getIvRandomRule());
            }
        }

        /**
         * 数据库加解密
         *
         * @param db {@link AesEncrypt}
         */
        public void setDb(AesEncrypt db) {
            this.db = db;
            AesDbConvert.setEnabled(db.isEnabled());
            AesDbConvert.setSecretKey(db.getSecretKey()); // 设置全局 aes 密钥
            if (db.isEnabled()) {
                if (Strings.isNullOrEmpty(db.getIvParameterSpec())) {
                    throw new IllegalArgumentException("spring.app.encrypt.db.ivParameterSpec='请指定16位固定IV'");
                } else {
                    AesDbConvert.setIV(db.getIvParameterSpec());
                }
            }
        }
    }

    /**
     * 密码加解密配置
     */
    @Getter
    @Setter
    @ToString
    public static class PwdEncrypt {
        /**
         * 加解密开关。 开启后登录密码需要使用 rsa 加密传输
         */
        private boolean enabled;
        /**
         * 加密类型，目前只支持 rsa
         */
        private String type;
        /**
         * 加密传输: 私钥
         */
        private String privateKey;
        /**
         * 加密传输: 公钥
         */
        private String publicKey;
    }

    /**
     * AES加密配置
     */
    @Getter
    @Setter
    @ToString
    public static class AesEncrypt {
        /**
         * 加解密开关。 true：是，false：否。
         */
        private boolean enabled;
        /**
         * 全局 aes 密钥， 32位
         */
        private String secretKey;
        /**
         * 固定 IV ， 16位。 设置固定 IV 之后同样内容每次加密结果一样。 默认为 null， 表示使用动态 IV
         */
        private String ivParameterSpec;
        /**
         * 设置动态 IV 生成规则， 默认： IvRandomRule.alphanumeric
         * <pre>
         *   IvRandomRule.alphanumeric     : RandomStringUtils.randomAlphanumeric 工具类生成 16 位随机 【A-Za-z0-9】 的字符串，解密时不校验 IV 生成规则
         *   IvRandomRule.timestamp        : 8 位日期(yyyyMMdd) + 8 位随机 【A-Za-z0-9】 的字符串，解密时提取前 8 位日期，检查日期是不是今天（注意：不是 24 小时有效期）
         *   IvRandomRule.dateAlphanumeric : 13位时间戳，前面补0，解密检查时间戳是不是今天范围（注意：不是 24 小时有效期）
         * </pre>
         */
        private IvRandomRule ivRandomRule;
        /**
         * API 接口数据包签名类型
         */
        private String signType;
    }

//
//    /**
//     * 应用程序配置枚举
//     * 来源：application.properties
//     */
//    public enum App {
//        env("当前环境：[local:本地开发环境|dev:远程开发环境|sit:系统测试|uat:验收测试|prod:生产]",
//                "spring.app.env", () -> Objects.toString(AppConfig.instance()().getEnv())),
//        version("当前运行版本号",
//                "spring.app.version", () -> Objects.toString(AppConfig.instance()().getVersion())),
//        ip("当前主机 IP 地址",
//                "spring.app.ip", () -> AppConfig.instance()().getIp()),
//        domain("应用域名；",
//                "spring.app.domain", () -> AppConfig.instance()().getDomain()),
//        pathRoot("应用文件根目录",
//                "spring.app.path-root", () -> AppConfig.instance()().getPathRoot()),
//        tokenExpired("token 默认过期时间(天)",
//                "spring.app.token-expired", () -> Objects.toString(AppConfig.instance()().getTokenExpired())),
//        autoTaskEnabled("定时任务开关，true：开启，false：关闭",
//                "spring.app.auto-task.enabled", () -> Objects.toString(AppConfig.instance()().isAutoTaskEnabled())),
//        ;
//        public final String key;
//        public final String comment;
//        private final Supplier<String> supplier;
//
//        App(final String comment, final String key, final Supplier<String> supplier) {
//            this.key = key;
//            this.comment = comment;
//            this.supplier = supplier;
//        }
//
//        /**
//         * 获取应用配置
//         *
//         * @return {@link String}
//         */
//        public String value() {
//            return Optional.ofNullable(supplier.get())
//                    .orElseThrow(() -> new NullPointerException(String.format("应用配置【%s】不能空", this.name())));
//        }
//
//        /**
//         * 获取应用配置，并追加字符
//         *
//         * @return {@link String}
//         */
//        public String append(final String append) {
//            return value().concat(append);
//        }
//    }

    /**
     * 应用文件对象
     */
    @Getter
    @Setter
    @ToString
    public static class AppFile {
        /**
         * 文件存储路径
         */
        private String directory;
        /**
         * 文件访问路径
         */
        private String url;
    }

    /**
     * 应用文件对象
     */
    @Getter
    @Setter
    @ToString
    public static class AppFiles {
        /**
         * 文件存储类型，可选参数： disk,minio,alioss
         */
        private Type type;

        /**
         * 应用文件根目录
         */
        private String directory;
        /**
         * 文件服务访问地址
         */
        private String server;
        /**
         * 临时文件存储路径及访问路径配置
         */
        private AppFile temp;
        /**
         * 用户头像文件存储路径及访问路径配置
         */
        private AppFile user;
    }

    public enum Type {
        // 磁盘存储
        DISK,
        // minio 存储
        MINIO,
        // 阿里云 oss 存储
        ALIOSS
    }

    /**
     * 定义所有路径枚举
     *
     * @author 谢长春 on 2018-10-2
     */
    public enum Path {
        TEMP("文件上传及临时文件存储目录", () -> instance().getFiles().getTemp().getDirectory()),
        USER("用户头像存储目录", () -> instance().getFiles().getUser().getDirectory()),
        ;
        /**
         * 枚举属性说明
         */
        public final String comment;
        /**
         * 获取文件目录
         */
        public final Supplier<String> directorySupplier;

        Path(String comment, Supplier<String> directorySupplier) {
            this.comment = comment;
            this.directorySupplier = directorySupplier;
        }
//
//        /**
//         * 获取文件路径操作对象
//         *
//         * @param names String[] 追加目录名或文件名
//         * @return FPath
//         */
//        public FPath fpath(String... names) {
//            return FPath.of(directorySupplier.get(), names);
//        }
//
//        /**
//         * 获取绝对路径
//         *
//         * @param names String[] 追加目录名或文件名
//         * @return String 文件绝对路径：d:\java
//         */
//        public String absolute(String... names) {
//            return FPath.of(directorySupplier.get(), names).absolute();
//        }
//
//        /**
//         * 获取文件对象
//         *
//         * @param names String[] 追加目录名或文件名
//         * @return File 文件对象
//         */
//        public File file(String... names) {
//            return FPath.of(directorySupplier.get(), names).file();
//        }

        /**
         * 获取绝对路径
         *
         * @param names String[] 追加目录名或文件名
         * @return String 文件绝对路径：d:\java
         */
        public String get(String... names) {
            final String dir = directorySupplier.get();
            if (Objects.isNull(names)) {
                return dir;
            }
            final StringBuilder builder = new StringBuilder(dir);
            if (!dir.endsWith("/")) {
                builder.append("/");
            }
            builder.append(Arrays.stream(names)
                    .flatMap(v -> Stream.of(v.split("/")))
                    .filter(StringUtils::isNoneBlank)
                    .collect(Collectors.joining("/"))
            );
            return builder.toString();
        }
    }

    /**
     * 定义所有路径枚举
     * url定义规则：
     * > 所有以 {} 占位的 使用 format 方法格式化参数
     * > 所有以 / 结尾的url可以追加路径
     * > 所有非 / 结尾的url不能追加路径
     *
     * @author 谢长春 on 2018-10-2
     */
    public enum URL {
        TEMP("临时文件访问路径", () -> instance().getFiles().getTemp().getUrl()),
        USER("用户头像访问路径", () -> instance().getFiles().getUser().getUrl()),
        ;
        /**
         * 枚举属性说明
         */
        public final String comment;
        /**
         * 获取访问地址
         */
        public final Supplier<String> urlSupplier;

        URL(String comment, Supplier<String> urlSupplier) {
            this.comment = comment;
            this.urlSupplier = urlSupplier;
        }

        /**
         * 获取路径
         *
         * @return String
         */
        public String value() {
            return urlSupplier.get();
        }

        /**
         * 追加路径
         *
         * @param args 参数集合
         * @return String
         */
        public String append(String... args) {
            return urlSupplier.get().concat(
                    String.join("/", args)
                            // 避免出现双斜杠
                            .replaceFirst("^/", "")
                            .replace("//", "/")
            );
        }

        /**
         * 格式化路径中 {} 指定的参数
         *
         * @param args 参数集合
         * @return String
         */
        public String format(String... args) {
            String result = urlSupplier.get();
            if (Objects.nonNull(args)) {
                for (Object value : args) { // 替换 url 参数占位符
                    result = result.replaceFirst("\\{(\\w+)?}", value.toString());
                }
            }
            return result;
        }

        /**
         * 格式化路径中 {} 指定的参数
         *
         * @param args 参数集合
         * @return String
         */
        public String format(final Map<String, String> args) {
            String result = urlSupplier.get();
            if (Objects.nonNull(args)) {
                for (Map.Entry<String, String> entry : args.entrySet()) {
                    // 替换 url 参数占位符
                    result = result.replace(String.format("{%s}", entry.getKey()), entry.getValue());
                }
            }
            return result;
        }
    }

}
